Skip to content

for...in walks an object literal's fields; a generic's instance makes other instances (JSON.stringify({x: 1})) - #832

Merged
ASDAlexander77 merged 3 commits into
mainfrom
fix-json-stringify-object-literal
Nov 12, 2026
Merged

ASDAlexander77 merged 3 commits into
mainfrom
fix-json-stringify-object-literal

Conversation

@ASDAlexander77

@ASDAlexander77 ASDAlexander77 commented Nov 12, 2026 •

Copy link
Copy Markdown
Owner

function main() { print(JSON.stringify({x: 1})); } built with --emit=exe exited with 0xC0000409 (--opt) or 3 (no --opt) and printed nothing, under every memory model.

It was not a lowering crash. The default library threw TypeError("JSON.stringify cannot read the fields of an object literal or a tuple") on purpose (DefaultLib #74 listed this as a limitation), and nothing caught it. Wrapped in try/catch, the program prints that message. To write the object instead, two things had to change in the compiler. The default-library side is ASDAlexander77/TypeScriptCompilerDefaultLib#95.

1. for...in over an object literal or a tuple

It used to desugar to an index loop over .length and failed to compile ("Can't resolve property 'length'"). Now it is made once for each field of the type:

for (let .i = 0; .i < N; .i++) { if (.i === 0) { const key = "a"; body } else if (.i === 1) { const key = "b"; body } }
  • The key is a string literal, so in the body obj[key] is that field, with its own type. A generic can walk an object literal's fields at compile time this way.
  • It is still a loop, so break and continue work.
  • An unnamed tuple field's key is its position, as in JavaScript. t["1"] and "1" in t now find that field too. Before, t["1"] printed nothing and "0" in t was false.
  • An accessor gives one key. A value field named get_x keeps its full name.
  • Each field's copy of the body gives its key to every name the compiler makes from a source location (MLIRHelper::anonymousNameCopy). Without that, an arrow function in the body redefined its symbol.
  • The object half of 00for_in.ts, commented out until now, is turned on.

2. An instance of a generic function can make other instances of it

GenericFunctionInfo::processing was one flag per generic function. So while f<number[]> was being made, a call of f<number> from its body was treated as recursion into an unfinished function, and failed with "not all generic types can be identified".

On main, print(JSON.stringify([1, 2])) does not compile when it is the program's first stringify. DefaultLib's json.ts passes only because it stringifies a number first.

The flag is now the set of instances being made. Any other instance is made at that point, up to 64 deep: f<T> calling f<T[]> would otherwise never end, and it reports an error, as before.

Tests

  • 00for_in_fields.ts
  • 00generic_instance_from_instance.ts

Both run under gc and own (compile and JIT, in their own entries) and under rc and none (through TSLANG_CORPUS). Both fail on main.

Verified locally (Windows x64)

  • ctest: 6992/6992 passed. The last change only touched the test file, and the 36 for-in, generic-instance and ownership-verifier tests were rerun after it.
  • With the DefaultLib branch, tests.ps1 passes for gc, rc, none and own, in release and debug, compiled and JIT.
  • The original program prints {"x":1} under every model, with and without --opt.
  • Not run: Linux/WSL.

Merge order

Merge this PR before ASDAlexander77/TypeScriptCompilerDefaultLib#95. The release workflow clones DefaultLib main, so its new json.ts cases fail with a compiler that does not have this change.

Known limits, not changed here

  • { 0: … } / { "0": … } is written by JSON.stringify as an array, and a named tuple as an object. Named tuples and object literals share one type, so the two cannot be told apart.
  • for (let k in obj) widens the key to string, so obj[k] is rejected. Use const.
  • Inside an arrow function in the body, the captured key is no longer a constant, so obj[key] there is a compile error.
  • Accessors come after value fields in the walk, not in source order.
  • A generic calling its own same instance (f(v, n - 1) from f<T>) still fails. It does on main too: the instance's type is registered only after its body is discovered.
  • Under -mm=own, an object literal holding a class instance ({ when: new Date(0) }) is still rejected by the existing own rule.
  • An uncaught exception in an exe still exits silently with 0xC0000409. That silence is what made this look like a crash.

🤖 Generated with Claude Code

ASDAlexander77 and others added 3 commits November 12, 2026 21:03
… other instances

`function main() { print(JSON.stringify({x: 1})); }` exited with 0xC0000409 (exit code 3
without --opt) and printed nothing under every memory model. The default library threw its
TypeError for an object literal, by design, and nothing caught it. Two compiler gaps stood
in the way of writing the object instead:

- for...in over an object literal or a tuple used `.length` and failed to compile. It is
  now made for each field of the type: `for (.i < N) { if (.i === 0) { const key = "a";
  body } else if ... }`, so `obj[key]` is that field with its own type and break/continue
  keep their meaning. An unnamed field's key is its position, and `t["1"]` and `"1" in t`
  find it, as in JavaScript.
- GenericFunctionInfo::processing was one flag per generic function, so while
  `f<number[]>` was being made, a call of `f<number>` from its body was taken for recursion
  into an unfinished function and failed with "not all generic types can be identified".
  `JSON.stringify([[1], [2]])` failed this way in any program that had not stringified a
  number first. It is now the set of instances being made; another instance is made then,
  up to 64 deep (`f<T>` calling `f<T[]>` would never end).

Tests: 00for_in_fields.ts, 00generic_instance_from_instance.ts (gc, own, rc, none; compile
and JIT), and the object half of 00for_in.ts, commented out until now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the body

- A value field named get_x / set_x was taken for an accessor and walked as `x`, which then
  was not found. Only a function field drops the prefix.
- The body is made once for each field, and an arrow function in it was named by its place
  in the source, so the second copy redefined the first's symbol. A copy's block now adds
  its key to every name made from a location while it is generated
  (MLIRHelper::anonymousNameCopy).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… default library)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 2f7764d into main Nov 12, 2026
8 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fix-json-stringify-object-literal branch November 12, 2026 22:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant